package com.hdu.kdk_es_orm.wrapper;

import com.google.common.collect.Sets;
import com.hdu.kdk_es_orm.annotation.LogicDelete;
import com.hdu.kdk_es_orm.core.QueryFilter;
import com.hdu.kdk_es_orm.utils.EsOrmException;
import com.hdu.kdk_es_orm.utils.LambdaUtils;
import com.hdu.kdk_es_orm.utils.SerializedLambda;
import lombok.val;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

import static com.hdu.kdk_es_orm.core.FilterType.EQ;
import static com.hdu.kdk_es_orm.core.FilterType.EXISTS;
import static com.hdu.kdk_es_orm.core.FilterType.EXP;
import static com.hdu.kdk_es_orm.core.FilterType.IN;
import static com.hdu.kdk_es_orm.core.FilterType.LIKE;
import static com.hdu.kdk_es_orm.core.FilterType.LIKE_LEFT;
import static com.hdu.kdk_es_orm.core.FilterType.LIKE_RIGHT;
import static com.hdu.kdk_es_orm.core.FilterType.NESTED;
import static com.hdu.kdk_es_orm.core.FilterType.NOT_EQ;
import static com.hdu.kdk_es_orm.core.FilterType.NOT_EXISTS;
import static com.hdu.kdk_es_orm.core.FilterType.NOT_LIKE;
import static com.hdu.kdk_es_orm.core.FilterType.NOT_LIKE_LEFT;
import static com.hdu.kdk_es_orm.core.FilterType.NOT_LIKE_RIGHT;
import static com.hdu.kdk_es_orm.core.FilterType.NOT_RANGE;
import static com.hdu.kdk_es_orm.core.FilterType.RANGE;
import static com.hdu.kdk_es_orm.utils.PropertyNamer.methodToProperty;
import static com.hdu.kdk_es_orm.utils.ReflectionUtils.getAnnotationOnFields;
import static com.hdu.kdk_es_orm.wrapper.EsWrapper.LogicOperator.AND;
import static com.hdu.kdk_es_orm.wrapper.EsWrapper.LogicOperator.OR;
import static com.hdu.kdk_es_orm.wrapper.EsWrapper.QueryType.FILTER_LOGIC_DELETE;
import static com.hdu.kdk_es_orm.wrapper.EsWrapper.QueryType.QUERY_ALL;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;

/**
 * EsWrapper
 */
public class EsWrapper<T> {

    private QueryFilter queryFilter;
    private int from = 0;
    private int size = 10;
    private final Set<String> includeFields = Sets.newHashSet();
    private OrderType orderType = OrderType.ASC;
    private String orderField;

    private final LogicOperator LOGIC_OPERATOR;
    private final Class<T> CLAZZ;

    public enum QueryType {
        FILTER_LOGIC_DELETE,
        QUERY_ALL
    }

    public enum OrderType {
        ASC,
        DESC
    }

    public enum LogicOperator {
        AND,
        OR
    }


    public static <T> EsWrapper<T> andOf(Class<T> clazz) {
        return new EsWrapper<>(clazz, QUERY_ALL, AND);
    }

    public static <T> EsWrapper<T> orOf(Class<T> clazz) {
        return new EsWrapper<>(clazz, QUERY_ALL, OR);
    }

    public static <T> EsWrapper<T> andOf(Class<T> clazz, QueryType queryType) {
        return new EsWrapper<>(clazz, queryType, AND);
    }

    public static <T> EsWrapper<T> orOf(Class<T> clazz, QueryType queryType) {
        return new EsWrapper<>(clazz, queryType, OR);
    }


    public static <T> EsWrapper<T> of(Class<T> clazz) {
        // 默认查询所有
        return new EsWrapper<>(clazz, QUERY_ALL, AND);
    }

    public static <T> EsWrapper<T> of(Class<T> clazz, QueryType queryType) {
        return new EsWrapper<>(clazz, queryType, AND);
    }

    public EsWrapper(Class<T> CLAZZ, QueryType queryType, LogicOperator logicOperator) {
        this.CLAZZ = CLAZZ;
        this.LOGIC_OPERATOR = logicOperator;

        val queryFilter = new QueryFilter();
        queryFilter.setType(NESTED);

        switch (logicOperator) {
            case AND:
                queryFilter.setMust(new ArrayList<>());
                break;
            case OR:
                queryFilter.setShould(new ArrayList<>());
                break;
            default:
                throw new EsOrmException("不支持的逻辑操作符");
        }
        if (queryType == FILTER_LOGIC_DELETE) {
            buildLogicQueryFilter()
                    .ifPresent(logicQueryFilter -> {
                        val must = ofNullable(queryFilter.getMust()).orElseGet(ArrayList::new);
                        must.add(logicQueryFilter);
                        queryFilter.setMust(must);
                    });
        }
        this.queryFilter = queryFilter;
    }


    public EsWrapper<T> eq(String field, Object eq) {
        return eq(true, field, eq);
    }


    public EsWrapper<T> eq(SFunction<T, ?> field, Object eq) {
        return eq(columnToString(field), eq);
    }

    public EsWrapper<T> eq(boolean condition, SFunction<T, ?> field, Object eq) {
        return eq(condition, columnToString(field), eq);
    }

    public EsWrapper<T> eq(boolean condition, String field, Object eq) {

        if (condition) {
            this.getMustOrShould().add(
                    new QueryFilter().type(EQ).field(field).eq(eq)
            );
        }

        return this;
    }


    public EsWrapper<T> allEq(Map<String, Object> params, boolean null2IsNull) {
        return allEq(true, params, null2IsNull);
    }

    public EsWrapper<T> allEq(boolean condition, Map<String, Object> params) {
        return allEq(condition, params, true);
    }

    public EsWrapper<T> allEq(Map<String, Object> params) {
        return allEq(true, params, true);
    }

    /**
     * null2IsNull：如果设置为 true，当 Map 中的 value 为 null 时，会调用 isNull 方法；
     * 如果设置为 false，则会忽略 value 为 null 的键值对。
     */

    public EsWrapper<T> allEq(boolean condition, Map<String, Object> params, boolean null2IsNull) {

        if (condition) {
            val eqQueryFilters = params.entrySet()
                    .stream()
                    .map(entry -> {
                        val field = entry.getKey();
                        val eqValue = entry.getValue();
                        if (Objects.nonNull(eqValue) || null2IsNull) {
                            return ofNullable(eqValue)
                                    .map(v -> new QueryFilter().type(EQ).field(field).eq(v))
                                    .orElseGet(() -> new QueryFilter().type(NOT_EXISTS).field(field));
                        } else {
                            return null;
                        }
                    })
                    .filter(Objects::nonNull)
                    .collect(toList());
            this.getMustOrShould().addAll(eqQueryFilters);
        }

        return this;
    }

    public EsWrapper<T> ne(String field, Object notEq) {
        return ne(true, field, notEq);
    }

    public EsWrapper<T> ne(SFunction<T, ?> field, Object notEq) {
        return ne(true, columnToString(field), notEq);
    }

    public EsWrapper<T> ne(boolean condition, SFunction<T, ?> field, Object notEq) {
        return ne(condition, columnToString(field), notEq);
    }

    public EsWrapper<T> ne(boolean condition, String field, Object notEq) {

        if (condition) {
            this.getMustOrShould().add(
                    new QueryFilter().type(NOT_EQ).field(field).notEq(notEq)
            );
        }

        return this;
    }

    public EsWrapper<T> in(String field, Collection<?> in) {
        return in(true, field, in);
    }

    public EsWrapper<T> in(SFunction<T, ?> field, Collection<?> in) {
        return in(columnToString(field), in);
    }

    public EsWrapper<T> in(boolean condition, SFunction<T, ?> field, Collection<?> in) {
        return in(condition, columnToString(field), in);
    }

    public EsWrapper<T> in(boolean condition, String field, Collection<?> in) {

        if (condition) {
            this.getMustOrShould().add(
                    new QueryFilter().type(IN).field(field).in(in)
            );
        }

        return this;
    }

    public EsWrapper<T> notIn(String field, Collection<?> notIn) {
        return this.notIn(true, field, notIn);
    }

    public EsWrapper<T> notIn(SFunction<T, ?> field, Collection<?> notIn) {
        return notIn(columnToString(field), notIn);
    }

    public EsWrapper<T> notIn(boolean condition, SFunction<T, ?> field, Collection<?> notIn) {
        return notIn(condition, columnToString(field), notIn);
    }

    public EsWrapper<T> notIn(boolean condition, String field, Collection<?> notIn) {

        if (condition) {
            this.getMustOrShould().add(
                    new QueryFilter().type(IN).notIn(notIn).field(field)
            );
        }

        return this;
    }

    public EsWrapper<T> lt(String field, Object lt) {
        return lt(true, field, lt);
    }

    public EsWrapper<T> lt(boolean condition, SFunction<T, ?> field, Object lt) {
        return lt(condition, columnToString(field), lt);
    }

    public EsWrapper<T> lt(SFunction<T, ?> field, Object lt) {
        return lt(columnToString(field), lt);
    }

    public EsWrapper<T> lt(boolean condition, String field, Object lt) {

        if (condition) {
            this.getMustOrShould().add(
                    new QueryFilter().lt(lt).type(RANGE).field(field)
            );
        }

        return this;
    }

    public EsWrapper<T> lte(SFunction<T, ?> field, Object lte) {
        return lte(columnToString(field), lte);
    }

    public EsWrapper<T> lte(boolean condition, SFunction<T, ?> field, Object lte) {
        return lte(condition, columnToString(field), lte);
    }

    public EsWrapper<T> lte(String field, Object lte) {
        return lte(true, field, lte);
    }

    public EsWrapper<T> lte(boolean condition, String field, Object lte) {

        if (condition) {
            getMustOrShould().add(new QueryFilter().lte(lte).type(RANGE).field(field));
        }

        return this;
    }


    public EsWrapper<T> gt(String field, Object gt) {
        return gt(true, field, gt);
    }


    public EsWrapper<T> gt(SFunction<T, ?> field, Object gt) {
        return gt(columnToString(field), gt);
    }

    public EsWrapper<T> gt(boolean condition, SFunction<T, ?> field, Object gt) {
        return gt(condition, columnToString(field), gt);
    }

    public EsWrapper<T> gt(boolean condition, String field, Object gt) {

        if (condition) {
            this.getMustOrShould().add(
                    new QueryFilter().gt(gt).field(field).type(RANGE)
            );
        }

        return this;
    }


    public EsWrapper<T> gte(String field, Object gte) {
        return gte(true, field, gte);
    }

    public EsWrapper<T> gte(SFunction<T, ?> field, Object gte) {
        return gte(true, columnToString(field), gte);
    }

    public EsWrapper<T> gte(boolean condition, SFunction<T, ?> field, Object gte) {
        return gte(condition, columnToString(field), gte);
    }

    public EsWrapper<T> gte(boolean condition, String field, Object gte) {

        if (condition) {
            this.getMustOrShould().add(
                    new QueryFilter().field(field).gte(gte).type(RANGE)
            );
        }

        return this;
    }


    public EsWrapper<T> eqBetween(boolean condition,
                                  SFunction<T, ?> field,
                                  Object gte,
                                  Object lte) {
        return eqBetween(condition, columnToString(field), gte, lte);
    }

    public EsWrapper<T> eqBetween(SFunction<T, ?> field,
                                  Object gte,
                                  Object lte) {
        return eqBetween(columnToString(field), gte, lte);
    }

    public EsWrapper<T> eqBetween(String filed,
                                  Object gte,
                                  Object lte) {
        return eqBetween(true, filed, gte, lte);
    }

    public EsWrapper<T> eqBetween(boolean condition,
                                  String filed,
                                  Object gte,
                                  Object lte) {
        if (condition) {
            this.getMustOrShould().add(
                    new QueryFilter().type(RANGE).gte(gte).lte(lte).field(filed)
            );
        }

        return this;
    }

    public EsWrapper<T> notEqBetween(boolean condition,
                                     SFunction<T, ?> field,
                                     Object gte,
                                     Object lte) {
        return notEqBetween(condition, columnToString(field), gte, lte);
    }

    public EsWrapper<T> notEqBetween(SFunction<T, ?> field,
                                     Object gte,
                                     Object lte) {
        return notEqBetween(columnToString(field), gte, lte);
    }

    public EsWrapper<T> notEqBetween(String filed,
                                     Object gte,
                                     Object lte) {
        return notEqBetween(true, filed, gte, lte);
    }

    public EsWrapper<T> notEqBetween(boolean condition,
                                     String filed,
                                     Object gte,
                                     Object lte) {
        if (condition) {
            this.getMustOrShould().add(
                    new QueryFilter().type(NOT_RANGE).gte(gte).lte(lte).field(filed)
            );
        }

        return this;
    }


    public EsWrapper<T> neqBetween(boolean condition,
                                   SFunction<T, ?> field,
                                   Object gt,
                                   Object lt) {
        return neqBetween(condition, columnToString(field), gt, lt);
    }

    public EsWrapper<T> neqBetween(SFunction<T, ?> field,
                                   Object gt,
                                   Object lt) {
        return neqBetween(columnToString(field), gt, lt);
    }

    public EsWrapper<T> neqBetween(String filed,
                                   Object gt,
                                   Object lt) {
        return neqBetween(true, filed, gt, lt);
    }

    public EsWrapper<T> neqBetween(boolean condition,
                                   String filed,
                                   Object lt,
                                   Object gt) {

        if (condition) {
            this.getMustOrShould().add(
                    new QueryFilter().type(RANGE).gt(lt).lt(gt).field(filed)
            );
        }

        return this;
    }

    public EsWrapper<T> notNeqBetween(boolean condition,
                                      SFunction<T, ?> field,
                                      Object gt,
                                      Object lt) {
        return notNeqBetween(condition, columnToString(field), gt, lt);
    }

    public EsWrapper<T> notNeqBetween(SFunction<T, ?> field,
                                      Object gt,
                                      Object lt) {
        return notNeqBetween(columnToString(field), gt, lt);
    }

    public EsWrapper<T> notNeqBetween(String filed,
                                      Object gt,
                                      Object lt) {
        return notNeqBetween(true, filed, gt, lt);
    }

    public EsWrapper<T> notNeqBetween(boolean condition,
                                      String filed,
                                      Object lt,
                                      Object gt) {

        if (condition) {
            this.getMustOrShould().add(
                    new QueryFilter().type(NOT_RANGE).gt(lt).lt(gt).field(filed)
            );
        }

        return this;
    }

    public EsWrapper<T> exists(String field) {
        return exists(true, field);
    }

    public EsWrapper<T> exist(SFunction<T, ?> field) {
        return exists(true, columnToString(field));

    }

    public EsWrapper<T> exist(boolean condition, SFunction<T, ?> field) {
        return exists(condition, columnToString(field));
    }

    public EsWrapper<T> nonNull(String field) {
        return exists(true, field);
    }

    public EsWrapper<T> nonNull(SFunction<T, ?> field) {
        return exists(true, columnToString(field));

    }

    public EsWrapper<T> nonNull(boolean condition, SFunction<T, ?> field) {
        return exists(condition, columnToString(field));
    }

    public EsWrapper<T> exists(boolean condition, String field) {

        if (condition) {
            this.getMustOrShould().add(new QueryFilter().field(field).type(EXISTS));
        }

        return this;
    }

    public EsWrapper<T> notExists(String field) {
        return notExists(true, field);
    }

    public EsWrapper<T> notExist(SFunction<T, ?> field) {
        return notExists(columnToString(field));
    }

    public EsWrapper<T> notExist(boolean condition, SFunction<T, ?> field) {
        return notExists(condition, columnToString(field));
    }

    public EsWrapper<T> isNull(String field) {
        return notExists(true, field);
    }

    public EsWrapper<T> isNull(SFunction<T, ?> field) {
        return notExists(columnToString(field));
    }

    public EsWrapper<T> isNull(boolean condition, SFunction<T, ?> field) {
        return notExists(condition, columnToString(field));
    }

    public EsWrapper<T> notExists(boolean condition, String field) {

        if (condition) {
            this.getMustOrShould().add(
                    new QueryFilter().field(field).type(NOT_EXISTS)
            );
        }

        return this;
    }

    public EsWrapper<T> like(String field, Object like) {
        return like(true, field, like);
    }

    public EsWrapper<T> like(SFunction<T, ?> field, Object like) {
        return like(true, columnToString(field), like);
    }

    public EsWrapper<T> like(boolean condition, SFunction<T, ?> field, Object like) {
        return like(condition, columnToString(field), like);
    }

    public EsWrapper<T> like(boolean condition, String field, Object like) {

        if (condition) {
            this.getMustOrShould().add(
                    new QueryFilter().field(field).like(like).type(LIKE)
            );
        }

        return this;
    }

    public EsWrapper<T> notLike(String field, Object notLike) {
        return notLike(true, field, notLike);
    }

    public EsWrapper<T> notLike(SFunction<T, ?> field, Object notLike) {
        return notLike(true, columnToString(field), notLike);
    }

    public EsWrapper<T> notLike(boolean condition, SFunction<T, ?> field, Object notLike) {
        return notLike(condition, columnToString(field), notLike);
    }

    public EsWrapper<T> notLike(boolean condition, String field, Object notLike) {

        if (condition) {
            this.getMustOrShould().add(
                    new QueryFilter().field(field).like(notLike).type(NOT_LIKE)
            );
        }

        return this;
    }

    public EsWrapper<T> likeLeft(String field, Object likeLeft) {
        return likeLeft(true, field, likeLeft);
    }

    public EsWrapper<T> likeLeft(SFunction<T, ?> field, Object likeLeft) {
        return likeLeft(true, columnToString(field), likeLeft);
    }

    public EsWrapper<T> likeLeft(boolean condition, SFunction<T, ?> field, Object likeLeft) {
        return likeLeft(condition, columnToString(field), likeLeft);
    }

    public EsWrapper<T> likeLeft(boolean condition, String field, Object likeLeft) {

        if (condition) {
            this.getMustOrShould().add(
                    new QueryFilter().field(field).like(likeLeft).type(LIKE_LEFT)
            );
        }

        return this;
    }

    public EsWrapper<T> notLikeLeft(String field, Object notLikeLeft) {
        return notLikeLeft(true, field, notLikeLeft);
    }

    public EsWrapper<T> notLikeLeft(SFunction<T, ?> field, Object notLikeLeft) {
        return notLikeLeft(true, columnToString(field), notLikeLeft);
    }

    public EsWrapper<T> notLikeLeft(boolean condition, SFunction<T, ?> field, Object notLikeLeft) {
        return notLikeLeft(condition, columnToString(field), notLikeLeft);
    }

    public EsWrapper<T> notLikeLeft(boolean condition, String field, Object notLikeLeft) {

        if (condition) {
            this.getMustOrShould().add(
                    new QueryFilter().field(field).like(notLikeLeft).type(NOT_LIKE_LEFT)
            );
        }

        return this;
    }

    public EsWrapper<T> likeRight(String field, Object likeRight) {
        return likeRight(true, field, likeRight);
    }

    public EsWrapper<T> likeRight(SFunction<T, ?> field, Object likeRight) {
        return likeRight(true, columnToString(field), likeRight);
    }

    public EsWrapper<T> likeRight(boolean condition, SFunction<T, ?> field, Object likeRight) {
        return likeRight(condition, columnToString(field), likeRight);
    }

    public EsWrapper<T> likeRight(boolean condition, String field, Object likeRight) {

        if (condition) {
            this.getMustOrShould().add(
                    new QueryFilter().field(field).like(likeRight).type(LIKE_RIGHT)
            );
        }

        return this;
    }


    public EsWrapper<T> notLikeRight(String field, Object notLikeRight) {
        return notLikeRight(true, field, notLikeRight);
    }

    public EsWrapper<T> notLikeRight(SFunction<T, ?> field, Object notLikeRight) {
        return notLikeRight(true, columnToString(field), notLikeRight);
    }

    public EsWrapper<T> notLikeRight(boolean condition, SFunction<T, ?> field, Object notLikeRight) {
        return notLikeRight(condition, columnToString(field), notLikeRight);
    }

    public EsWrapper<T> notLikeRight(boolean condition, String field, Object notLikeRight) {

        if (condition) {
            this.getMustOrShould().add(
                    new QueryFilter().field(field).like(notLikeRight).type(NOT_LIKE_RIGHT)
            );
        }

        return this;
    }


    public EsWrapper<T> script(String script) {
        return script(true, script);
    }

    public EsWrapper<T> script(boolean condition, String script) {
        if (condition) {
            this.getMustOrShould().add(new QueryFilter().type(EXP).exp(script));
        }
        return this;
    }

    public EsWrapper<T> size(int size) {
        this.setSize(size);
        return this;
    }

    public EsWrapper<T> from(int from) {
        this.setFrom(from);
        return this;
    }

    @SafeVarargs
    public final EsWrapper<T> includeFields(SFunction<T, ?>... includeFields) {
        if (includeFields == null || includeFields.length == 0) {
            return this;
        }
        return this.includeFields(
                Arrays.stream(includeFields)
                        .map(this::columnToString)
                        .distinct()
                        .toArray(String[]::new)
        );
    }

    public EsWrapper<T> includeFields(String... includeFields) {
        if (includeFields != null && includeFields.length > 0) {
            this.includeFields.addAll(Arrays.asList(includeFields));
        }
        return this;
    }

    public EsWrapper<T> orderBy(String orderField, OrderType orderType) {
        this.orderField = orderField;
        this.orderType = orderType;
        return this;
    }

    public EsWrapper<T> orderBy(SFunction<T, ?> orderField, OrderType orderType) {
        return orderBy(columnToString(orderField), orderType);
    }

    public Optional<String> getOrderField() {
        return ofNullable(orderField);
    }

    public OrderType getOrderType() {
        return orderType;
    }

    public void setQueryFilter(QueryFilter queryFilter) {
        this.queryFilter = queryFilter;
    }

    public QueryFilter getQueryFilter() {
        return queryFilter;
    }

    public void setFrom(int from) {
        this.from = from;
    }

    public int getFrom() {
        return from;
    }

    public int getSize() {
        return size;
    }

    public void setSize(int size) {
        this.size = size;
    }

    public String[] getIncludeFields() {
        return includeFields.toArray(new String[0]);
    }

    public Class<T> getCLAZZ() {
        return this.CLAZZ;
    }


    private String columnToString(SFunction<T, ?> column) {
        return getColumn(LambdaUtils.resolve(column));
    }

    private String getColumn(SerializedLambda lambda) throws EsOrmException {
        return methodToProperty(lambda.getImplMethodName());
    }

    private List<QueryFilter> getMustOrShould() {
        switch (LOGIC_OPERATOR) {
            case AND:
                return queryFilter.getMust();
            case OR:
                return queryFilter.getShould();
            default:
                throw new EsOrmException("queryFilter type must be MUST or SHOULD");
        }
    }

    private Optional<QueryFilter> buildLogicQueryFilter() {
        return ofNullable(getAnnotationOnFields(CLAZZ, LogicDelete.class))
                .map(
                        logicDelete -> new QueryFilter()
                                .field(logicDelete.filedName())
                                .type(NOT_EQ)
                                .notEq(logicDelete.deleteValue())
                );
    }


}
